﻿#region Copyright(C) 2018 FI Ltd. All rights reserved.

// ==================================================================================================
// Copyright(C) 2018 FI Ltd. All rights reserved.
//
// SYSTEM NAME	:   FI.Net Application Framework 
// COMPONENT ID :   FI.EntityFramework
// COMPONENT DESC:  
//
// CREATED DATE/BY: 18 Aug 2018 / Tian Mi
//
// REVISION HISTORY:	DATE/BY			SR#/CS/PM#/OTHERS		DESCRIPTION OF CHANGE
// ==================================================================================================

#endregion

using System;
using System.Collections.Generic;
using System.Data.Common;
using System.Data.Entity;
using System.Data.Entity.Core;
using System.Data.Entity.Core.Mapping;
using System.Data.Entity.Core.Metadata.Edm;
using System.Data.Entity.Core.Objects;
using System.Data.Entity.Infrastructure;
using System.Linq;

namespace FI.EntityFramework.Interception
{
    /// <summary>
    /// The base class for validating and rebuilding the <see cref="DbCommand"/>.
    /// </summary>
    internal abstract class DbCommandHelper : IDbCommandHelper
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="DbCommandHelper"/> class.
        /// </summary>
        /// <param name="command">The command.</param>
        internal DbCommandHelper(DbCommand command)
        {
            this.Command = command;
        }

        private void AddClause(string columnName, DbParameter parameter, params IList<CommandClause>[] targets)
        {
            foreach (var target in targets)
            {
                target.Add(new CommandClause(columnName, parameter.ParameterName));
            }
        }

        private DbParameter AddParameter(string propertyName, DbDataRecord values, Func<string, string> nameFormatter, Func<DbDataRecord, int, object> valueFormatter)
        {
            var index = values.GetOrdinal(propertyName);
            var name = (null == nameFormatter) ? propertyName : nameFormatter(propertyName);
            var value = (null == valueFormatter) ? values.GetValue(index) : valueFormatter(values, index);

            var parameter = this.Command.CreateParameter();
            parameter.ParameterName = this.GetParameterName(name);
            parameter.Value = value;
            this.Command.Parameters.Add(parameter);
            return parameter;
        }

        private EntityKey CreateEntityKey(DeleteCommandStatement statement, MappingFragment mappingFragment)
        {
            var entitySet = ((EntityTypeMapping)mappingFragment.TypeMapping).EntitySetMapping.EntitySet;
            var entitySetName = String.Format("{0}.{1}", entitySet.EntityContainer.Name, entitySet.Name);
            var keyProperties = entitySet.ElementType.KeyProperties;
            var keyValues = (
                from m in mappingFragment.PropertyMappings.OfType<ScalarPropertyMapping>()
                join c in statement.DeleteCondition
                on m.Column.Name equals c.Column
                where keyProperties.Contains(m.Property)
                select new EntityKeyMember(m.Property.Name, this.Command.Parameters[c.Parameter].Value)
            ).ToArray();
            return new EntityKey(entitySetName, keyValues);
        }

        private IEnumerable<ScalarPropertyMapping> GetFlatPropertyMappings(IEnumerable<PropertyMapping> propertyMappings)
        {
            foreach (var propertyMapping in propertyMappings)
            {
                var scalarPropertyMapping = propertyMapping as ScalarPropertyMapping;
                if (null != scalarPropertyMapping)
                {
                    yield return scalarPropertyMapping;
                }

                var complexPropertyMapping = propertyMapping as ComplexPropertyMapping;
                if (null != complexPropertyMapping)
                {
                    var complexTypeMapping = complexPropertyMapping.TypeMappings.Single();
                    foreach (var flatPropertyMapping in this.GetFlatPropertyMappings(complexTypeMapping.PropertyMappings))
                    {
                        yield return flatPropertyMapping;
                    }
                }
            }
        }

        private bool TryGetObjectEntryWithMapping(DeleteCommandStatement statement, ObjectContext objectContext, out ObjectStateEntry objectEntry, out MappingFragment mappingFragment)
        {
            var mapping = objectContext.MetadataWorkspace.GetItems<EntityContainerMapping>(DataSpace.CSSpace).Single();
            var mappingFragments = (
                from s in mapping.EntitySetMappings
                from t in s.EntityTypeMappings
                from f in t.Fragments
                let table = f.StoreEntitySet.MetadataProperties["Table"]
                where statement.Table.Equals((table != null) ? table.Value : f.StoreEntitySet.ElementType.Name)
                select f
            ).ToArray();

            foreach (var fragment in mappingFragments)
            {
                var key = this.CreateEntityKey(statement, fragment);
                if (objectContext.ObjectStateManager.TryGetObjectStateEntry(key, out objectEntry)
                    &&
                    objectEntry.State == EntityState.Deleted)
                {
                    mappingFragment = fragment;
                    return true;
                }
            }

            objectEntry = null;
            mappingFragment = null;
            return false;
        }

        /// <summary>
        /// Gets the delete statement from the current command.
        /// </summary>
        /// <returns>The delete statement object.</returns>
        protected abstract DeleteCommandStatement GetDeleteCommandStatement();

        /// <summary>
        /// Formats parameter name for the current command.
        /// </summary>
        /// <param name="arg">The given argument i.e. property name or numeric.</param>
        /// <returns>The parameter name for the current command.</returns>
        protected abstract string GetParameterName(object arg);

        /// <summary>
        /// Re-generate the delete command text for the current command.
        /// </summary>
        /// <param name="statement">The delete statement object.</param>
        /// <returns>
        /// The command text for the current command.
        /// </returns>
        protected abstract string GenerateDeleteCommandText(DeleteCommandStatement statement);

        #region IDbCommandHelper

        /// <summary>
        /// Resets the current command for FI audit trial.
        /// </summary>
        /// <param name="dbContext">The context to associate.</param>
        public void ResetDeleteCommand(DbContext dbContext)
        {
            ObjectStateEntry entry;
            MappingFragment fragment;
            var originalStatement = this.GetDeleteCommandStatement();
            var objectContext = ((IObjectContextAdapter)dbContext).ObjectContext;
            if (!this.TryGetObjectEntryWithMapping(originalStatement, objectContext, out entry, out fragment))
            {
                return;
            }

            var deleteCondition = new List<CommandClause>();
            var updateCondition = new List<CommandClause>();
            var updateContent = new List<CommandClause>();
            this.Command.Parameters.Clear();
            foreach (var propertyMapping in this.GetFlatPropertyMappings(fragment.PropertyMappings))
            {
                if (propertyMapping.Property.Name == EntityFrameworkConfigurationView.VersionNoField)
                {
                    var parameter = this.AddParameter(propertyMapping.Property.Name, entry.OriginalValues, (p) => String.Format("Original{0}", p), (r, i) => r.GetInt32(i));
                    this.AddClause(propertyMapping.Column.Name, parameter, updateCondition);
                    parameter = this.AddParameter(propertyMapping.Property.Name, entry.OriginalValues, (p) => String.Format("Current{0}", p), (r, i) => r.GetInt32(i) + 1);
                    this.AddClause(propertyMapping.Column.Name, parameter, deleteCondition, updateContent);
                    continue;
                }

                if (propertyMapping.Property.Name == EntityFrameworkConfigurationView.TransactionIdField)
                {
                    var parameter = this.AddParameter(propertyMapping.Property.Name, entry.OriginalValues, null, (r, i) => String.Format("{0}^$", r.GetString(i)));
                    this.AddClause(propertyMapping.Column.Name, parameter, updateContent);
                    continue;
                }

                if (propertyMapping.Property.Name == EntityFrameworkConfigurationView.LastUpdatedByField
                    ||
                    propertyMapping.Property.Name == EntityFrameworkConfigurationView.LastUpdatedTimeField)
                {
                    var parameter = this.AddParameter(propertyMapping.Property.Name, entry.OriginalValues, null, null);
                    this.AddClause(propertyMapping.Column.Name, parameter, updateContent);
                    continue;
                }

                if (entry.EntitySet.ElementType.KeyProperties.Contains(propertyMapping.Property)
                    ||
                    propertyMapping.Property.ConcurrencyMode == ConcurrencyMode.Fixed)
                {
                    var parameter = this.AddParameter(propertyMapping.Property.Name, entry.OriginalValues, null, null);
                    this.AddClause(propertyMapping.Column.Name, parameter, deleteCondition, updateCondition);
                    continue;
                }
            }
            var currentStatement = new DeleteCommandStatement(originalStatement.Schema, originalStatement.Table, deleteCondition, updateCondition, updateContent);
            this.Command.CommandText = this.GenerateDeleteCommandText(currentStatement);
        }

        /// <summary>
        /// Validates the current command if it's a delete command.
        /// </summary>
        /// <returns>
        /// if set to <c>true</c> the current command is delete command.
        /// </returns>
        public abstract bool ValidateDeleteCommand();

        /// <summary>
        /// Gets the command being executed.
        /// </summary>
        /// <value>
        /// The command being executed.
        /// </value>
        public DbCommand Command
        {
            get;
            private set;
        }

        #endregion IDbCommandHelper
    }
}